Using WebSockets with Etherlink Nodes

Blockchain Etherlink EVM Websocket Tezos
Published on 2025/02/03
 Using WebSockets with Etherlink Nodes


Using WebSockets with Etherlink Nodes

This is a joint post with Nomadic Labs.

With version 0.14, the Etherlink EVM node supports requests over both REST HTTP and WebSockets. WebSocket is a communication protocol that provides bidirectional, real-time, and persistent communication between a client and server over a single, long-lived TCP connection. The advantage of WebSocket over REST HTTP is to provide faster and more efficient data exchange without the overhead of repeatedly opening and closing connections with the server and performing TLS handshakes. Another advantage of WebSocket for Etherlink is the newly added support for the eth_subscribe method. The server can now send notifications for new blocks (newHeads), transactions in the pool (pendingTransactions) and transaction logs (logs) directly to clients, in real time without the need for continuous polling. This opens up use cases for applications that want to do blockchain analytics and monitoring with real-time updates from the network.

You can use any websocket framework to connect to Etherlink nodes. This post includes examples for the Web3js and ws NPM modules and the wscat and websocat command-line tools. The rest of this blog post will explore how to use WebSockets in the Etherlink network.

EVM Node Setup

ℹ️ Please refer to the official Etherlink documentation for instructions on how to setup and run an Etherlink EVM node.

To activate the support for WebSockets in your EVM node, an experimental feature flag must be set. Add the field enable_websocket to the experimenteal_features section of the configuration file (assuming location $EVM_NODE_DATA_DIR/config.json), or add this section as such:

  "experimental_features": {
    "enable_websocket": true
  }

Once this change is made, restart your EVM observer node. If you do not have a running EVM observer node, you can quickly get started thanks to these new options of the run command (the example below is for the Etherlink testnet on Tezos ghostnet):

octez-evm-node run observer \
  --data-dir "$EVM_NODE_DATA_DIR" \
  --dont-track-rollup-node \
  --network testnet \
  --init-from-snapshot

Connecting to a WebSocket

Programmatically

Assuming your EVM node is accessible locally on 127.0.0.1:8545, you can create a WebSocket connection with the following (in Javascript, either in the browser or with node.js):

const WebSocket = require('ws'); // only for node.js
const ws = new WebSocket('ws://127.0.0.1:8545/ws');

// Wait for connection to be established
await new Promise(resolve => {
  if (ws.readyState !== ws.OPEN) {
    ws.on("open", resolve);
  } else {
    resolve();
  }
})

ws.onmessage = (msg) => {
  const data = JSON.parse(msg.data);
  // Process websocket messages from server, here we just print them in
  // the console
  console.log('Received data on websocket:', data)
}

Using CLI tools

One can also use command line tools to connect to WebSockets. Popular options include wscat and websocat.

These tools are primarily used to test WebSocket connections. We use them here as WebSocket clients, openning a connection to the server (the EVM node), sending messages over the WebSocket, and displaying messages received from the server in response. Simply put, they allow interactive communication with a WebSocket server.

Connecting with wscat

wscat -c ws://127.0.0.1:8545/ws
Connected (press CTRL+C to quit)
>

Once you see this prompt you can interact with the server by sending JSON requests (one per line).

The following JSON-RPC call allows to query the latest block for instance.

Connected (press CTRL+C to quit)
> {"jsonrpc": "2.0","method": "eth_getBlockByNumber","params":["latest", false],"id": 1}
< ... // Response from EVM node will be shown here

Connecting with websocat

websocat ws://127.0.0.1:8545/ws -E

There is no prompt for websocat but it works the same as wscat.

Sending JSON-RPC Requests on the WebSocket

All Etherlink JSON-RPCs that can be made on the EVM node with REST HTTP can also be made on the WebSocket. For instance, we can issue the following to retrieve the latest block of the chain.

ws.send(JSON.stringify({
  jsonrpc: "2.0",
  method: "eth_getBlockByNumber",
  params:["latest", false],
  id: 1
}));

When receiving this message, the server will respond on the websocket and we will see the response in the console (thanks to our ws.onmessage above).

Received data on websocket: {
  jsonrpc: '2.0',
  result: {
    number: '0x100d0ba',
    hash: '0x63d7190ea6de62568d40f0b48b02172130e803136375bdda5f2fa702e323b7f5',
    parentHash: '0x3baff862b1d24c4775b93e201adac92e817b14ae3561742740026b7344b63272',
    nonce: '0x0000000000000000',
    sha3Uncles: '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347',
    logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
    transactionsRoot: '0xe1d6a2219eb75e6f879f6773e939e00df004643d16fbae004d6123f2feb1a09f',
    stateRoot: '0x35421a162a343279b8e2e6a2b7d0073484182aed4c4dc02f7130fa20914d77a3',
    receiptsRoot: '0x5202257adc3e3cb0a95c2bc3c2217eaed7397f54ff4a021cc23797d341410fd0',
    miner: '0xcf02b9ca488f8f6f4e28e37aa1bdd16b3f1b2ad8',
    difficulty: '0x0',
    totalDifficulty: '0x0',
    extraData: '0x',
    size: '0x279',
    gasLimit: '0x4000000000000',
    gasUsed: '0x1589f2',
    timestamp: '0x67856fca',
    transactions: [
      '0x4076ab3c3e2345107082ff13023bead045bcef966c89f8fed1b9f939ac438e81'
    ],
    uncles: [],
    baseFeePerGas: '0x3b9aca00',
    prevRandao: '0x0000000000000000000000000000000000000000000000000000000000000000'
  },
  id: 1
}

Subscribing to Events with eth_subscribe

The method eth_susbcribe is only available on WebSocket connections and allows clients to subscribe to real-time updates for specific blockchain events, such as new blocks, pending transactions, or contract event logs.

The response to an eth_subscribe message consists of two parts:

  • Subscription Acknowledgment: Upon successfully creating a subscription, the server responds with a unique subscription ID, confirming the subscription.
  • Event Notifications: As relevant events occur (e.g., a new block or log event), the server sends push notifications containing the subscription ID and event data.

The subscription ID can be used with the method eth_unsubscribe to stop receiving updates for this particular event.

New Heads

With eth_subscribe it is possible to receive notifications about new heads of the Etherlink chain in real-time.

With Messages on the WebSocket

Subscribing to new heads events can be done by sending the following message:

ws.send(JSON.stringify({
  jsonrpc: "2.0",
  method: "eth_subscribe",
  params:["newHeads"],
  id: 2
}));

The first message that is sent by the server on the WebSocket contains the subscription ID which we can later use to unsubscribe and to identify messages when several subscriptions are active:

Received data on websocket: { jsonrpc: '2.0', result: '0xc46a38daf682aba1b16da00078d80885', id: 2 }

Then the EVM node will send notifications on the websocket with the blocks contents:

Received data on websocket: {
  jsonrpc: '2.0',
  method: 'eth_subscription',
  params: {
    result: {
      number: '0x1010e2d',
      hash: '0x89a9e075976cae83f069e79d2d49e49db7b804dd5d2854a1cc55531527c0e297',
      parentHash: '0x52c05c75291beeeedf5f92647e1b06011949a988cf424b4379e7ac328ea72bce',
      nonce: '0x0000000000000000',
      sha3Uncles: '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347',
      logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
      transactionsRoot: '0x2d0871bce6286632fbba8ed76e770a9187e2a8451cfd916780e1bb38aa712678',
      stateRoot: '0xa6f0be4207991f8e76818b4d391da821d9e15296cf6cedcbb3a272aadec756be',
      receiptsRoot: '0x2d5a6b0dffd96fdae789537754039a801b7f62d85b655fa10ed479e47e134ea1',
      miner: '0xcf02b9ca488f8f6f4e28e37aa1bdd16b3f1b2ad8',
      difficulty: '0x0',
      totalDifficulty: '0x0',
      extraData: '0x',
      size: '0x258',
      gasLimit: '0x4000000000000',
      gasUsed: '0x0',
      timestamp: '0x67868f27',
      transactions: [],
      uncles: [],
      baseFeePerGas: '0x3b9aca00',
      prevRandao: '0x0000000000000000000000000000000000000000000000000000000000000000'
    },
    subscription: '0xc46a38daf682aba1b16da00078d80885'
  }
}

Received data on websocket: {
  jsonrpc: '2.0',
  method: 'eth_subscription',
  params: {
    result: {
      number: '0x1010e2e',
      hash: '0x12376ec73ef995f0c5b26b06257ff3c14fa2489f72d80ccfffe5cb377142c417',
      parentHash: '0x89a9e075976cae83f069e79d2d49e49db7b804dd5d2854a1cc55531527c0e297',
      nonce: '0x0000000000000000',
      sha3Uncles: '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347',
      logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
      transactionsRoot: '0x776ced21d739017fa60a4835a66bfce7857db7d7dd663f7394618d7104f1ec2b',
      stateRoot: '0x8d329a161f0c5da6d7326257ddad11af5811607ba622d462ce3f21385bebe116',
      receiptsRoot: '0x40d25aabdc333e976801012685dd05debaf1cfdf584307075e0514cec2da9370',
      miner: '0xcf02b9ca488f8f6f4e28e37aa1bdd16b3f1b2ad8',
      difficulty: '0x0',
      totalDifficulty: '0x0',
      extraData: '0x',
      size: '0x279',
      gasLimit: '0x4000000000000',
      gasUsed: '0x2af0c1',
      timestamp: '0x67868f2c',
      transactions: [Array],
      uncles: [],
      baseFeePerGas: '0x3b9aca00',
      prevRandao: '0x0000000000000000000000000000000000000000000000000000000000000000'
    },
    subscription: '0xc46a38daf682aba1b16da00078d80885'
  }
}

When one wishes to stop receiving updates on new heads, it is sufficient to call the eth_unsubscribe method with the subscription ID:

ws.send(JSON.stringify({
  jsonrpc: "2.0",
  method: "eth_unsubscribe",
  params:["0xc46a38daf682aba1b16da00078d80885"],
  id: 3
}));
Received data on websocket: { jsonrpc: '2.0', result: true, id: 3 }

With web3.js

This example uses the web3.js JavaScript library to subscribe to new head block events and display the number of each block in the console.

⚠️ Warning: Notice that we use WebsocketProvider otherwise eth.subscribe will not be available.

const { Web3 } = require('web3');
const web3 = new Web3(new Web3.providers.WebsocketProvider('ws://127.0.0.1:8545/ws'));

// Subscribe to new block headers
const newBlocksSubscription = await web3.eth.subscribe('newBlockHeaders');
newBlocksSubscription.on('error', error => {
    console.log('Error when subscribing to New block header:', error);
});

newBlocksSubscription.on('data', blockhead => {
    // Just log the block number
    console.log(`New block: ${blockhead.number}`);
});

Block number will be displayed in the console as they are produced:

New block: 16846276
New block: 16846277
New block: 16846278
New block: 16846279
New block: 16846280
New block: 16846281
New block: 16846282
New block: 16846283

To unsubscribe from updates, use the unsubscribe() method, as in this example:

newBlocksSubscription.unsubscribe().then(() => {
    console.log('Unsubscribed from new block headers.');
});

Then the program disconnects from the websocket and stops writing block numbers to the console.

New Pending Transactions

Using any of these software packages, you can subscribe to multiple events on a single WebSocket connection. For example, this code uses web3.js to subscribe to both new blocks and new pending transactions:

const { Web3 } = require('web3');
const web3 = new Web3(new Web3.providers.WebsocketProvider('ws://127.0.0.1:8545/ws'));

// Subscribe to new block headers
const newBlocksSubscription = await web3.eth.subscribe('newBlockHeaders');
newBlocksSubscription.on('data', blockhead => {
    // Just log the block number
    console.log(`New block: ${blockhead.number}`);
});

// Subscribe to pending transactions
const txsSubscription = await web3.eth.subscribe('pendingTransactions');
txsSubscription.on('data', tx => {
    // Log full transaction object
    console.log('New pending transaction:', tx);
});

When we're done we can unsubscribe from both:

await newBlocksSubscription.unsubscribe();
await txsSubscription.unsubscribe();

Contract Logs

Another useful kind of events for which we can receive notifications are logs (emitted by a contract). We can provide the address of the smart contract and the topics we are interested in.

The following snippet simply dumps all logs emitted by smart contracts in Etherlink on the console.

const { Web3 } = require('web3');
const web3 = new Web3(new Web3.providers.WebsocketProvider('ws://127.0.0.1:8545/ws'));

const logsSubscription = await web3.eth.subscribe('logs', {});
logsSubscription.on('data', console.log);

And we'll be notified of things like

{
  address: '0xb1ea698633d57705e93b0e40c1077d46cd6a51d8',
  topics: [
    '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
    '0x000000000000000000000000f60e84b902e097c1e722e13fed0f14e52f5c5496',
    '0x0000000000000000000000002fe633088dbf960821f4a25923a60497dbd06af5'
  ],
  data: '0x0000000000000000000000000000000000000000000000000214e8348c4f0000',
  blockNumber: '0x1011582',
  transactionHash: '0xf8ae811940ed83a0dc99404beff8f8c1c7572c68d6dfda34f569785ed21544c6',
  transactionIndex: '0x0',
  blockHash: '0x09c6d3cfb7514d4a43a4011f00ba1a32212b37597f5bd8ed929fcf05682e5844',
  logIndex: '0x0',
  removed: false
}
{
  address: '0x1a71f491fb0ef77f13f8f6d2a927dd4c969ece4f',
  topics: [
    '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
    '0x000000000000000000000000f60e84b902e097c1e722e13fed0f14e52f5c5496',
    '0x0000000000000000000000002fe633088dbf960821f4a25923a60497dbd06af5'
  ],
  data: '0x000000000000000000000000000000000000000000000002b5e3af16b1880000',
  blockNumber: '0x1011583',
  transactionHash: '0xc7c16dd2c54091653adae7c8ddbe77e49570b8875d9be6b94eeb6fc2689388c4',
  transactionIndex: '0x0',
  blockHash: '0xbbe906ef295dd5b456d28b0307602dbc909623632ac0fff20cf6ab1d79be27ea',
  logIndex: '0x0',
  removed: false
}
...

Notifying ERC20 Transfers

This final section presents a small useful application of subscriptions.

For instance we may want to be notified of ERC20 transfers on the Etherlink Testnet. The following snippet does this by subscribing to logs events for a particular topic.

const { Web3 } = require('web3');
const web3 = new Web3(new
Web3.providers.WebsocketProvider('ws://127.0.0.1:8545/ws'));

const abi = [
  {
    constant: true,
    inputs: [],
    name: "symbol",
    outputs: [{name: "", type: "string"}],
    payable: false,
    stateMutability: "view",
    type: "function"
  },
  {
    constant: true,
    inputs: [],
    name: "decimals",
    outputs: [{name: "", type: "uint8"}],
    payable: false,
    stateMutability: "view",
    type: "function"
  },
];

const options = {
   topics: [web3.utils.keccak256Wrapper('Transfer(address,address,uint256)')]
};

async function showTransfer(event) {
  if (event.topics.length == 3) {
    let transaction = web3.eth.abi.decodeLog(
      [
        { type: "address", name: "from", indexed: true },
        { type: "address", name: "to", indexed: true },
        { type: "uint256", name: "value", indexed: false }
      ],
      event.data,
      event.topics
    );
    const contract = new web3.eth.Contract(abi, event.address);
    const decimals = await contract.methods.decimals().call();
    const symbol = await contract.methods.symbol().call();
    const amount = Number(transaction.value) / Number(10n ** decimals);
    console.log(`${amount} ${symbol}: ${transaction.from} --> ${transaction.to}`);
  }
};

const erc20Transfers = await web3.eth.subscribe('logs', options);
erc20Transfers.on('error', (err) => { throw err });
erc20Transfers.on('data', showTransfer);

In the code above, we filter logs on their first topic, which we want to be Transfer calls (the entrypoint signature needs to be hashed).

Then we call the function showTransfer on each such new log event. We first decode the Transfer call to get meaningful values for the parameters, then we fetch the decimals and token symbol from the ERC20 contract to display correct values and units.

You will see in the console all ERC20 transfers on Etherlink displayed as such:

0.15 WXTZ: 0xf60e84b902e097c1e722e13FED0f14E52f5c5496 --> 0x2Fe633088Dbf960821f4A25923A60497dBd06aF5
50 eUSD: 0xf60e84b902e097c1e722e13FED0f14E52f5c5496 --> 0x2Fe633088Dbf960821f4A25923A60497dBd06aF5
0.01 BTC: 0xf60e84b902e097c1e722e13FED0f14E52f5c5496 --> 0x2Fe633088Dbf960821f4A25923A60497dBd06aF5
0.1 WETH: 0xf60e84b902e097c1e722e13FED0f14E52f5c5496 --> 0x2Fe633088Dbf960821f4A25923A60497dBd06aF5

Conclusion

These are just a few examples of how you can use WebSockets to efficiently monitor the activity on Etherlink and respond immediately when certain events happen. This new support will make using existing Ethereum tooling even more straightforward. You can use this code as the basis for an indexer or to respond in real time when users make transactions.

Contact us for more information about using WebSockets with Etherlink.

|